package db

import (
	"context"
	"errors"
	"fmt"
	"time"

	"v8s/logger"
	resourcename "v8s/resource-name"

	"v8s-api/internal"
	"v8s-api/internal/configuration"

	"go.mongodb.org/mongo-driver/bson"
	"go.mongodb.org/mongo-driver/bson/primitive"
	"go.mongodb.org/mongo-driver/mongo"
	"go.mongodb.org/mongo-driver/mongo/options"

	"github.com/sethvargo/go-password/password"
	"go.uber.org/zap"
)

type (
	Project struct {
		ID    internal.ProjectID `json:"project-id" bson:"project-id"`
		Tasks []Task             `json:"tasks" bson:"tasks"`
		Cred  LoggingCredentials `json:"logging-credentials" bson:"logging-credentials,omitempty"`
	}

	Task struct {
		ID        internal.TaskID           `json:"task-id" bson:"task-id"`
		RN        resourcename.ResourceName `json:"resource-name" bson:"resource-name"`
		TN        TaskName                  `json:"task-name" bson:"task-name"`
		Status    TaskStatus                `json:"status" bson:"status"`
		LogStatus LoggingStatus             `json:"logging-status,omitempty" bson:"logging-status,omitempty"`

		Metadata map[string]interface{} `json:"metadata" bson:"metadata,omitempty"`
		Compose  map[string]interface{} `json:"compose" bson:"compose,omitempty"`

		CreatedAt  time.Time `json:"created-at" bson:"created-at"`
		ModifiedAt time.Time `json:"modified-at" bson:"modified-at"`

		Clusters map[ClusterName]ClusterDetails `json:"clusters" bson:"clusters"`
	}

	LoggingCredentials struct {
		User     string `json:"user" bson:"user,omitempty"`
		Password string `json:"password" bson:"password,omitempty"`
	}

	ClusterName string
	TaskName    string

	ClusterDetails struct {
		Status    ClusterStatus               `json:"status" bson:"status"`
		Resources []Resource                  `json:"resources" bson:"resources"`
		Services  map[ServiceName]ServiceInfo `json:"services" bson:"services"`
	}

	Resource struct {
		Kind   string         `json:"kind" bson:"kind"`
		Name   string         `json:"name" bson:"name"`
		Status ResourceStatus `json:"status" bson:"status"`
	}

	ServiceName    string
	ResourceStatus string
	TaskStatus     string
	LoggingStatus  string
	ClusterStatus  string

	Cluster struct {
		Cluster string        `json:"cluster" bson:"cluster"`
		Status  ClusterStatus `json:"status" bson:"status"`
	}

	Container struct {
		Name   string `json:"name" bson:"name"`
		Status string `json:"status" bson:"status"`
	}

	ServiceInfo struct {
		Endpoints []string `bson:"endpoints,omitempty"`
	}

	ProjectsRepository struct {
		log *zap.SugaredLogger

		client     *MongoClient
		properties configuration.MongoProperties
	}

	ProjectResult struct {
		Project *Project
		Error   error
	}
)

const (
	ProjectsCollection = "projects"

	ClusterStatusRunning ClusterStatus = "running"

	TaskStatusCreationRequest   TaskStatus = "creation-request"
	TaskStatusDeleted           TaskStatus = "deleted"
	TaskStatusDeletionRequest   TaskStatus = "deletion-request"
	TaskStatusError             TaskStatus = "error"
	TaskStatusResumeRequest     TaskStatus = "resume-request"
	TaskStatusRunning           TaskStatus = "running"
	TaskStatusSuspended         TaskStatus = "suspended"
	TaskStatusSuspensionRequest TaskStatus = "suspension-request"
	TaskStatusUpdateRequest     TaskStatus = "update-request"

	statusUpdateMaxRetries    = 10
	statusUpdateBackoffPeriod = time.Millisecond * 200

	CreationRequestLoggingStatus = "creation-request"
	PatchRequstLoggingStatus     = "patch-request"
	SuccessLoggingStatus         = "success"
	ErrorLoggingStatus           = "error"
)

var (
	TaskIsAbsent = errors.New("task do not exists")
)

func CreateProjectsRepository(client *MongoClient, properties configuration.MongoProperties) *ProjectsRepository {
	repository := &ProjectsRepository{
		log: logger.Logger().Named("db-projects-repository").Sugar(),

		client:     client,
		properties: properties,
	}

	err := repository.init()
	if err != nil {
		panic(fmt.Errorf("failed to init repository: %w", err))
	}

	return repository
}

func (repository *ProjectsRepository) IsAlive(ctx context.Context) error {
	return repository.client.Ping(ctx, nil)
}

// CreateTask
// Возможные кейсы:
// - проект отсутствует
// - проект есть, отсутствует задача c таким RN
// - проект есть, пристуствует задача с таким RN
func (repository *ProjectsRepository) CreateTask(ctx context.Context, id internal.ProjectID, task Task) (Task, error) {
	// Сначала проверим существование проекта, если нет, создадим с пустыми задачами
	insertResult, err := repository.collection().UpdateOne(ctx,
		bson.M{"project-id": id},
		bson.M{"$setOnInsert": bson.M{"project-id": id}},
		options.Update().SetUpsert(true))
	if err != nil {
		return Task{}, fmt.Errorf("failed to check existence of project '%s': %w", id, err)
	}

	if insertResult.MatchedCount == 0 {
		task.LogStatus = CreationRequestLoggingStatus

	}

	// можно не думать о локах, в следующий раз когда controller будет обрабатывать проект, подхватит новую задачу
	// rn должен быть уникальный среди всех задач, иначе не сможем потом делать get by rn
	where := bson.M{"project-id": id, "tasks": bson.M{"$not": bson.M{"$elemMatch": bson.M{"resource-name": task.RN}}}}
	//var dbtask map[string]interface{}
	//bytes, err := json.Marshal(task)

	//if err != nil {
	//	return Task{}, fmt.Errorf("failed to unmarshal resource-name in new task '%s' in project '%s': %w", task.RN, id, err)
	//}
	//err = json.Unmarshal(bytes, dbtask)
	//dbtask["resource-name"] = task.RN.String()
	update := bson.M{"$push": bson.M{"tasks": task}}

	result, err := repository.collection().UpdateOne(ctx, where, update)
	if err != nil {
		return Task{}, fmt.Errorf("failed to insert new task '%s' in project '%s': %w", task.RN, id, err)
	}

	if result.MatchedCount == 0 {
		return Task{}, fmt.Errorf("failed to create task '%s': task already exists in project '%s'", task.RN, id)
	}

	return task, nil
}

func (repository *ProjectsRepository) GetTask(ctx context.Context, rn resourcename.ResourceName) (Task, bool, error) {
	result := repository.collection().FindOne(ctx, bson.M{"tasks.resource-name": rn.String()})
	if result.Err() == mongo.ErrNoDocuments {
		return Task{}, false, nil
	}

	if result.Err() != nil {
		return Task{}, false, fmt.Errorf("failed to find task '%s': %w", rn, result.Err())
	}

	project := &Project{}
	if err := result.Decode(project); err != nil {
		return Task{}, false, fmt.Errorf("failed to decode project with task '%s': %w", rn, err)
	}

	for _, task := range project.Tasks {
		if task.RN.String() == rn.String() {
			return task, true, nil
		}
	}

	return Task{}, false, fmt.Errorf("task '%s' not found in project", rn)
}

func (repository *ProjectsRepository) FindTasks(ctx context.Context) ([]Task, error) {
	tasks, err := repository.findTasks(ctx, mongo.Pipeline{
		{{Key: "$unwind", Value: "$tasks"}},
		{{Key: "$replaceRoot", Value: bson.M{"newRoot": "$tasks"}}},
	})
	if err != nil {
		return nil, fmt.Errorf("failed to find tasks: %w", err)
	}

	return tasks, nil
}

func (repository *ProjectsRepository) FindActiveTasks(ctx context.Context) ([]Task, error) {
	filter := bson.M{"status": bson.M{"$nin": bson.A{TaskStatusDeletionRequest, TaskStatusDeleted}}}

	tasks, err := repository.findTasks(ctx, mongo.Pipeline{
		{{Key: "$match", Value: bson.M{"tasks": bson.M{"$elemMatch": filter}}}},
		{{Key: "$unwind", Value: "$tasks"}},
		{{Key: "$replaceRoot", Value: bson.M{"newRoot": "$tasks"}}},
		{{Key: "$match", Value: filter}},
	})
	if err != nil {
		return nil, fmt.Errorf("failed to find tasks: %w", err)
	}

	return tasks, nil
}

func (repository *ProjectsRepository) FindTasksUpdatedAfter(ctx context.Context, point time.Time, metadata map[string]interface{}, limit int) ([]Task, error) {
	filter := bson.M{"modified-at": bson.M{"$gt": primitive.NewDateTimeFromTime(point)}}
	if len(metadata) > 0 {
		filter = and(filter, bson.M{"metadata": metadata})
	}

	tasks, err := repository.findTasks(ctx, mongo.Pipeline{
		{{Key: "$match", Value: bson.M{"tasks": bson.M{"$elemMatch": filter}}}},
		{{Key: "$unwind", Value: "$tasks"}},
		{{Key: "$replaceRoot", Value: bson.M{"newRoot": "$tasks"}}},
		{{Key: "$match", Value: filter}},
		{{Key: "$limit", Value: limit}},
	})
	if err != nil {
		return nil, fmt.Errorf("failed to aggregate tasks with date '%s', metadata: '%v': %w", point, metadata, err)
	}

	return tasks, nil
}

func (repository *ProjectsRepository) CreateCredentials(ctx context.Context, id internal.ProjectID, user string) error {
	// Customize the list of symbols.
	gen, err := password.NewGenerator(&password.GeneratorInput{
		Symbols: "!@#$%^()",
	})
	if err != nil {
		return fmt.Errorf("failed to create password generator for project '%s': %w", id, err)
	}
	password, err := gen.Generate(8, 2, 2, false, false)
	if err != nil {
		return fmt.Errorf("failed to generate password for project '%s': %w", id, err)
	}
	where := bson.M{"project-id": id, "logging-credentials": bson.M{"$exists": false}}
	update := bson.M{"$set": bson.M{"logging-credentials": bson.M{"user": user, "password": password}}}

	_, err = repository.collection().UpdateOne(ctx, where, update)
	if err != nil {
		return fmt.Errorf("failed to create credentials for project '%s': %w", id, err)
	}

	return nil
}

func (repository *ProjectsRepository) UpdateTask(ctx context.Context, rn resourcename.ResourceName, compose, metadata map[string]interface{}) (Task, error) {
	now := time.Now()

	filter := bson.M{"resource-name": rn.String(), "status": TaskStatusRunning}

	condition := bson.M{
		"collector-locked-at": bson.M{"$exists": false},
		"tasks":               bson.M{"$elemMatch": filter},
	}

	update := set(
		bson.M{"tasks.$.compose": compose},
		bson.M{"tasks.$.status": TaskStatusUpdateRequest},
		bson.M{"tasks.$.logging-status": PatchRequstLoggingStatus},
		bson.M{"tasks.$.metadata": metadata},
		bson.M{"tasks.$.modified-at": primitive.NewDateTimeFromTime(now)},
	)

	result, err := repository.collection().UpdateOne(ctx, condition, update)
	if err != nil {
		return Task{}, fmt.Errorf("failed to update task '%s': %w", rn, err)
	}

	if result.MatchedCount == 0 {
		return Task{}, fmt.Errorf("task '%s' not in status '%s' or locked", rn, TaskStatusRunning)
	}

	return Task{
		RN:         rn,
		Status:     TaskStatusUpdateRequest,
		Metadata:   metadata,
		Compose:    compose,
		CreatedAt:  now.Add(-1 * time.Hour),
		ModifiedAt: now,
	}, nil
}

func (repository *ProjectsRepository) UpdateTaskStatus(ctx context.Context, rn resourcename.ResourceName, newStatus TaskStatus, previous []TaskStatus) error {
	filter := bson.M{"resource-name": rn.String()}
	if len(previous) > 0 {
		filter = and(filter, bson.M{"status": bson.M{"$in": previous}})
	}

	condition := bson.M{
		"collector-locked-at": bson.M{"$exists": false},
		"tasks":               bson.M{"$elemMatch": filter},
	}

	update := set(
		bson.M{"tasks.$.status": newStatus},
		bson.M{"tasks.$.modified-at": primitive.NewDateTimeFromTime(time.Now())},
	)

	result := repository.collection().FindOneAndUpdate(ctx, condition, update)
	if result.Err() == mongo.ErrNoDocuments {
		count, err := repository.collection().CountDocuments(ctx, bson.M{"tasks.resource-name": rn.String()})
		if err != nil {
			return fmt.Errorf("task '%s' was not found at locked state, and existence check failed: %w", rn, err)
		}

		if count < 1 {
			return fmt.Errorf("task '%s' status was not updated: %w", rn, TaskIsAbsent)
		}

		// if task exists, then we can check is task status suitable for transition
		count, err = repository.collection().CountDocuments(ctx, bson.M{"tasks": bson.M{"$elemMatch": filter}})
		if err != nil {
			return fmt.Errorf("task '%s' was found at locked state, but status check failed: %w", rn, err)
		}

		if count < 1 {
			return fmt.Errorf("task '%s' status cannot be transitioned to '%s'", rn, newStatus)
		}

		// if task status is suitable for transition, then it is locked, and we can retry several times
		retries := 0
		for retries < statusUpdateMaxRetries {
			retries++
			time.Sleep(statusUpdateBackoffPeriod)

			result := repository.collection().FindOneAndUpdate(ctx, condition, update)
			if errors.Is(result.Err(), mongo.ErrNoDocuments) {
				continue
			}

			if result.Err() != nil {
				return fmt.Errorf("failed to update task status with retries: %w", result.Err())
			}

			return nil
		}

		return fmt.Errorf("task '%s' is locked", rn)
	}

	if result.Err() != nil {
		return fmt.Errorf("failed to find task '%s': %w", rn, result.Err())
	}

	return nil
}

func (repository *ProjectsRepository) findTasks(ctx context.Context, pipeline mongo.Pipeline) ([]Task, error) {
	results, err := repository.collection().Aggregate(ctx, pipeline)
	if err != nil {
		return nil, fmt.Errorf("failed to aggregate tasks: %w", err)
	}

	if results.Err() != nil {
		return nil, fmt.Errorf("failed to aggregate tasks: %w", err)
	}

	tasks := make([]Task, 0)
	if err := results.All(ctx, &tasks); err != nil {
		return nil, fmt.Errorf("failed to decode tasks: %w", err)
	}

	return tasks, nil
}

func (repository *ProjectsRepository) collection() *mongo.Collection {
	return repository.client.Database(repository.properties.Database).Collection(ProjectsCollection)
}

func (repository *ProjectsRepository) init() error {
	return nil
}

func merge(items ...bson.M) bson.M {
	result := bson.M{}

	for _, item := range items {
		for property, value := range item {
			if result[property] != nil {
				panic(fmt.Sprintf("failed to merge operations, '%s' is duplicated", property))
			}

			result[property] = value
		}
	}

	return result
}

func set(items ...bson.M) bson.M {
	return bson.M{"$set": merge(items...)}
}

func and(items ...bson.M) bson.M {
	result := bson.A{}

	for _, item := range items {
		result = append(result, item)
	}

	return bson.M{"$and": result}
}
